﻿#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

//结构体回顾
//结构体变量的创建和初始化
//struct Stu
//{
//	char name[20];//名字
//	int age;//年龄
//	char sex[5];//性别
//	char id[20];//学号
//}s3, s4;//全局变量
//int main()
//{
//	//按照结构体成员的顺序初始化 
//	struct Stu s1 = { "张三", 20, "男", "20230818001" };//局部变量
//
//	//按照指定的顺序初始化 
//	struct Stu s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex =
//   "女" };
//	return 0;
//}

//结构的特殊声明
	//匿名结构体类型
//struct Stu
//{
//	char c;
//	int i;
//	double d;
//}s = { 'x', 100, 3.14 };
//int main()
//{
//	printf("%c %d %lf", s.c, s.i, s.d);
//	return 0;
//}
    //匿名的结构体类型，如果没有对结构体类型重命名的话，基本上只能使用⼀次。

//结构的自引用
    //在结构中包含⼀个类型为该结构本身的成员是否可以呢？
//struct Node
//{
//	int data;
//	struct Node next;
//};
    //其实是不行的，因为⼀个结构体中再包含一个同类型的结构体变量，这样结构体变量的大小就会无穷的大，是不合理的。

    //正确的自引用方式：
//struct Node 
//{
//    int data;
//    struct Node* next; //对其改用结构体指针变量就不会出现上面计算结构体变量的大小进入死循环而无穷大了
//};

    //匿名的结构体类型是不能实现上面结构体自引用的效果的
    //并且对结构体 typedef 重命名时，内部的struct是不能省略的：
//typedef struct
//{
//    int data;
//    Node* next;
//}Node;
    //因为Node是对前面的匿名结构体类型的重命名产生的，但是在匿名结构体内部提前使用 Node 类型来创建成员变量，这是不行的。

//结构体内存对齐(计算结构体的大小)  (极其重要)
//对齐规则
    //首先我们要了解掌握结构体的对齐规则：
    //1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处 (偏移量就是结构体成员到结构体变量起始位置的字节长度)
    //2.其他成员变量要对齐到某个数字（对齐数）的整数倍的地址处。
        //对齐数 = 编译器默认的一个对齐数 与 该成员变量大小的较小值。
        //在VS中默认的值为 8 
    //3.结构体总大小为最大对齐数（结构体中每个成员变量都有一个对齐数，所有对齐数中最大的）的整数倍。
    //4.如果嵌套了结构体的情况，嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处，
        //结构体的整体大小就是所有最大对齐数（含嵌套结构体中内成员的对齐数）的整数倍。

//练习1：
//struct S
//{
//    char c1; //1
//    int i; //4
//    char c2; //1
//};
//
//int main()
//{
//    struct S s = { 0 };
//    printf("%zd\n", sizeof(s)); //12
//    return 0;
//}
    //以练习1为例对结构体内存对齐的规则进行解释：
    //首先第一个成员是char c1，大小为1个字节，放在 到结构体变量起始位置偏移量为0(也就是起始位置) 的地址处

    //再第二个成员是int i，大小为4个字节，对于对齐数规则来说，变量大小小于默认对齐数，所以int i的对齐数为4
    //而紧接char c1的地址到结构体变量起始位置偏移量为1，并不是4的倍数，故要浪费一些空间
    //当地址到结构体变量起始位置偏移量为4时，即满足规则放入第二个成员，
    //占用4个字节空间，放到偏移量为7处截至，此时总共占用8个字节大小

    //最后到第三个成员char c2，大小为1个字节，所以char c2的对齐数为1
    //偏移量为8是1的倍数，所以对应的地址能够放入第三个成员，占用1个字节空间，此时总共占用9个字节大小

    //那9就是结构体的大小吗？这就要看规则的第三条，结构体总大小为最大对齐数的整数倍，而struct S的最大对齐数为4
    //而9不是4的倍数，故空间还要往后浪费3个字节大小，到12，所以struct S的大小为12

//练习2：
//struct S2
//{
//    char c1;
//    char c2;
//    int i;
//};
//
//int main()
//{
//    struct S2 s = { 0 };
//    printf("%zd\n", sizeof(s)); //8
//    return 0;
//}

//练习3：
//struct S3
//{
//    double d;
//    char c;
//    int i;
//};
//
//int main()
//{
//    struct S3 s = { 0 };
//    printf("%zd\n", sizeof(s)); //16
//    return 0;
//}

//练习4-结构体嵌套问题
//struct S4
//{
//    char c1;
//    struct S3 s3;
//    double d;
//};
//
//int main()
//{
//    struct S4 s1 = { 0 };
//    printf("%zd\n", sizeof(s1)); //32
//    return 0;
//}
    //练习3就涉及到结构体嵌套，也就是对齐规则的第四条
    //首先char c1放到结构体变量起始位置，偏移量为0

    //再看struct S3，由第四条规则我们知道：嵌套的结构体对齐到自己的成员内中最大对齐数的整数倍处，
    //而struct S3内成员最大的对齐数为8，所以对齐到8的倍数的位置，也就是浪费7个字节空间放入到偏移量为8的位置
    //而struct S3本身结构体大小为16，故占用16个字节大小，放到偏移量为23处截至，此时总共占用24个字节大小

    //最后到第三个成员double d，大小为8个字节，偏移量为24是8的倍数，能够放入，放到偏移量为31处截至，此时总共占用32个字节大小
    
    //那32就是结构体的大小吗？
    //这就继续要看规则第四条：结构体的整体大小就是所有最大对齐数（含嵌套结构体中内成员的对齐数）的整数倍
    //嵌套结构体 struct S3 中内成员的最大对齐数为8，
    //结构体 struct S4 成员的最大对齐数为8，故struct S4大小为8的整数倍，32满足条件，即为struct S4的大小

    //总体来说：结构体的内存对齐是拿空间来换取时间的做法。
    //那在设计结构体的时候，我们既要满足对齐，又要节省空间，如何做到：让占用空间小的成员尽量集中在一起

//修改默认对齐数
    // #pragma 这个预处理指令，可以改变编译器的默认对齐数

//#pragma pack(1)//设置默认的对齐数为1 
//struct s
//{
//    char c1; 
//    int i; 
//    char c2;
//};
//
//int main()
//{
//    struct s s = { 0 };
//    printf("%zd\n", sizeof(s)); //6
//    return 0;
//}
//
//#pragma pack()//取消设置的对齐数，还原为默认

//结构体传参
//打印结构体内容
//struct S
//{
//    int arr[1000];
//    int n;
//    double d;
//};
//
//void Print1(struct S s)
//{
//    int i = 0;
//    for (i = 0; i < 5; i++)
//    {
//        printf("%d ", s.arr[i]);
//    }
//    printf("%d ", s.n);
//    printf("%lf ", s.d);
//}
//
//int main()
//{
//    struct S s = { {1, 2, 3, 4, 5}, 100, 3.14 };
//    Print1(s);
//    return 0;
//}

    //首先结构体 struct S 内arr数组就有1000个元素，数组总大小就已经是4000个字节了，结构体占用了非常大的空间
    //而在调用该结构体时，形参 s 也要开辟同样大小的空间，这样会非常浪费空间

    //我们知道地址的大小就只有4/8个字节
    //所以可以考虑利用结构体指针传参：
//struct S
//{
//    int arr[1000];
//    int n;
//    double d;
//};
//
//void Print2(const struct S* ps)
//{
//    int i = 0;
//    for (i = 0; i < 5; i++)
//    {
//        printf("%d ", ps->arr[i]);
//    }
//    printf("%d ", ps->n);
//    printf("%lf ", ps->d);
//}
//
//int main()
//{
//    struct S s = { {1, 2, 3, 4, 5}, 100, 3.14 };
//    Print2(&s);
//    return 0;
//}
    //这一次仅仅传给形参结构体地址，也就是说形参只需要开辟4/8个字节大小的空间，但也能通过间接访问来获得结构体的内容
    //好处就是极大的节省了空间
    //结论：结构体传参的时候，要传结构体的地址。

//结构体实现位段
//什么是位段
    //位段的声明和结构是类似的，有两个不同：
    //1.位段的成员必须是 int、unsigned int 或 signed int 
    //2.位段的成员名后边有一个冒号和一个数字

//位段式的结构(二进制位)
//struct S
//{
//    int _a : 2; //-->只占2个bit位
//    int _b : 5; //-->只占5个bit位
//    int _c : 10;//-->只占10个bit位
//    int _d : 30;//-->只占30个bit位
//};
//
//int main()
//{
//    printf("%zd\n", sizeof(struct S));//8
//    return 0;
//}
    //传统的结构体大小应该为16，通过位段的方式可以大大节省内存
    //但我们再想想，上面的位段总共就47个bit位，6*8=48，差不多也就6个字节，可结果却是8个字节，这是为什么？

//位段的内存分配
//一个例子 
//struct S
//{
//    char a : 3;
//    char b : 4;
//    char c : 5;
//    char d : 4;
//};
//
//int main()
//{
//    struct S s = { 0 };
//    s.a = 10;
//    s.b = 12;
//    s.c = 3;
//    s.d = 4;
//    return 0;
//}
    //首先，对于VS来说：1.位段的内存存放一个字节，就是8个bit位，是从右往左分配
    //                  2.而且一个字节剩余的空间如果不足以下一个成员使用的时候，是会浪费，放到下一个字节当中
    // a 的值为10，二进制为1010，但 a 分配的最大空间就3个bit位，所以最高位要舍弃，放入010
    //所以第一个字节还有5个bit位，b的值为12，二进制为1100，b的分配空间为4个bit，可以放入1100
    //此时只剩下1个bit位，而c要分配的空间是5个bit位，所以不会放入，而是放到第2个字节当中
    //所以第1个字节放入的是01100010，第1个0是初始化的，浪费的空间

    //c的值为3，二进制为00011，放到第2个字节右边，此时剩下3个bit位，而d所需空间为4个bit位，仍然不够，所以放到第3个字节中
    //所以第2个字节放入的是00000011，前3个0是初始化的，浪费的空间

    //d的值为4，二进制为0100，放到第3个字节右边，所以第3个字节放入的是00000100，前4个0是初始化的，浪费的空间
    //所以位段总共占用3个字节，为01100010 00000011 00000100，化成十六进制为0x62 03 04
    //通过调试也是得到这样的结果，也说明了VS中位段的确是按照上面的规则存放的

//位段使用的注意事项
    //位段的几个成员共有同一个字节，这样有些成员的起始位置并不是某个字节的起始位置，那么这些位置处是没有地址的。
    //内存中每个字节分配一个地址，一个字节内部的bit位是没有地址的。
    //而位段针对的空间大小就是以bit位为单位，
    //所以不能对位段的成员使用 & 操作符，这样就不能使用 scanf 直接给位段的成员输入值，
    //只能是先输入放在一个变量中，然后赋值给位段的成员。
//struct S
//{
//    int _a : 2;
//    int _b : 5;
//    int _c : 10;
//    int _d : 30;
//};
//
//int main()
//{
//    struct S sa = { 0 };
//    //scanf("%d", &sa._b);//这是错误的 
//
//    //正确的示范： 
//    int b = 0;
//    scanf("%d", &b);
//    sa._b = b;
//    return 0;
//}